Skip to content

Update plugin ZTools 提供商 v1.0.1#309

Merged
lzx8589561 merged 1 commit into
ZToolsCenter:mainfrom
kaineooo:plugin/f-provider
Jul 10, 2026
Merged

Update plugin ZTools 提供商 v1.0.1#309
lzx8589561 merged 1 commit into
ZToolsCenter:mainfrom
kaineooo:plugin/f-provider

Conversation

@kaineooo

@kaineooo kaineooo commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

插件信息

  • 名称: ZTools 提供商
  • 插件ID: f-provider
  • 版本: 1.0.1
  • 描述: OCR + 翻译提供商集合(百度/谷歌/有道/微软翻译、微信 OCR)
  • 作者: kaineooo
  • 类型: 更新

本次变更

  • feat: ZTools OCR + 翻译提供商插件(截图识别 / 代码翻译 / manage)
  • chore: 调整插件命名空间
  • chore: 调整资源url
  • feat: 微信 OCR 原生模块支持 macOS(libwxocr.dylib)
  • feat: 截图识别结果改为独立窗口展示(左图右文 + 拖动缩放)

截图 / 演示

自检清单

  • plugin.json 的 name / title / version / description / author 字段均已检查
  • 已移除调试日志、未使用文件、敏感信息(.env、token、密钥等)
  • 本次 PR 的 diff 仅涉及 plugins/f-provider/ 目录
  • 已在本地 ZTools 客户端实际加载并测试过此插件,主要功能正常
  • 同意以仓库声明的开源协议发布此插件

此 PR 由 ztools-plugin-cli 自动管理:每次 ztools publish 在分支上追加一个 commit,PR 链接保持不变。

- feat: ZTools OCR + 翻译提供商插件(截图识别 / 代码翻译 / manage)
- chore: 调整插件命名空间
- chore: 调整资源url
- feat: 微信 OCR 原生模块支持 macOS(libwxocr.dylib)
- feat: 截图识别结果改为独立窗口展示(左图右文 + 拖动缩放)
@kaineooo kaineooo marked this pull request as ready for review July 10, 2026 13:51

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the screen OCR feature to display results in a separate, borderless sub-window (screen-ocr-result.html) instead of the main plugin window. The main window now acts as an orchestrator that handles engine checking, capturing, and invoking the sub-window, while the new sub-window handles image rendering, zooming, panning, and text selection. The review feedback highlights several critical and medium-severity issues, including a redundant image-loading call that resets user zoom/pan on fallback data injection, potential crashes from un-checked display or destroyed window instances, a potential negative scale rendering bug in extreme viewport sizes, TypeScript type conflicts with setTimeout, and Vue 3 reactivity issues when using sparse arrays for tracking flash states.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +54 to +56
if (typeof data.logo === 'string') logo.value = data.logo
if (data.image) nextTick(loadImage)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

loadData 中,手动调用 nextTick(loadImage) 是多余的,因为代码中已经对 image 变量设置了 watch 监听器。

更严重的是,由于主窗口在 800ms 后会进行一次兜底的数据注入,如果保留这行代码,兜底注入时会再次触发 loadImage(),从而调用 fitView()。这会导致用户在窗口打开后 800ms 内进行的任何缩放或拖拽操作被强制重置,极大地影响用户体验。建议移除此处的冗余调用,完全交由 watch 处理。

  if (typeof data.logo === 'string') logo.value = data.logo
}

Comment on lines +135 to +141
const display = window.ztools.getPrimaryDisplay()
// workArea 同为 DIP,含任务栏扣除,比 size 更安全
const workArea = display.workArea
const screenWDip = workArea.width
const screenHDip = workArea.height
// scaleFactor:如 1 / 1.25 / 1.5 / 1.9 / 2
const scaleFactor = display.scaleFactor || 1

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

在获取主显示器信息时,建议对 display 进行空值保护。在某些多显示器切换、无显示器或特定系统环境下,getPrimaryDisplay() 可能会返回 nullundefined,直接访问其属性会导致运行时崩溃。

  const display = window.ztools.getPrimaryDisplay()
  // workArea 同为 DIP,含任务栏扣除,比 size 更安全
  const workArea = display?.workArea || { x: 0, y: 0, width: 1280, height: 800 }
  const screenWDip = workArea.width
  const screenHDip = workArea.height
  // scaleFactor:如 1 / 1.25 / 1.5 / 1.9 / 2
  const scaleFactor = display?.scaleFactor || 1

Comment on lines +221 to 234
/** 把数据注入子窗口:调用其 window.__loadScreenOcrResult。 */
function injectData(
win: BrowserWindow.WindowInstance,
data: { image: string; lines: OcrLine[]; isDark: boolean; logo?: string }
): void {
try {
// 序列化为安全的 JSON,避免引号/换行破坏 JS 字符串
const payload = JSON.stringify(data)
const code = `window.__loadScreenOcrResult && window.__loadScreenOcrResult(${payload});`
win.webContents.executeJavaScript(code)
} catch (_) {
/* ignore:兜底注入会再试 */
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

在向子窗口注入数据时,由于存在 800ms 的延迟兜底注入(setTimeout),如果用户在 800ms 内快速关闭了窗口,或者窗口创建失败返回了空值,直接调用 win.webContents 会抛出异常。建议在执行注入前,先检查 win 实例是否存在且未被销毁。

/** 把数据注入子窗口:调用其 window.__loadScreenOcrResult。 */
function injectData(
  win: any,
  data: { image: string; lines: OcrLine[]; isDark: boolean; logo?: string }
): void {
  try {
    if (!win || (typeof win.isDestroyed === 'function' && win.isDestroyed()) || !win.webContents) {
      return
    }
    // 序列化为安全的 JSON,避免引号/换行破坏 JS 字符串
    const payload = JSON.stringify(data)
    const code = 'window.__loadScreenOcrResult && window.__loadScreenOcrResult(' + payload + ');'
    win.webContents.executeJavaScript(code)
  } catch (_) {
    /* ignore:兜底注入会再试 */
  }
}

Comment on lines +72 to +73
const copied = ref('')
let copiedTimer: number | null = null

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

在混合了 Node.js 和浏览器 DOM 类型的 Electron/Vite 环境中,window.setTimeout 的返回值类型可能会在 numberNodeJS.Timeout 之间产生冲突,导致 TypeScript 编译失败。建议使用 ReturnType<typeof setTimeout> 来声明定时器变量的类型,以确保跨平台编译的兼容性。

const copied = ref('')
let copiedTimer: ReturnType<typeof setTimeout> | null = null

Comment on lines +119 to +128
function computeFitScale(): number {
const vp = viewportRef.value
const vw = vp ? vp.clientWidth : window.innerWidth
const vh = vp ? vp.clientHeight : window.innerHeight
if (!naturalWidth.value || !naturalHeight.value || !vw || !vh) return 1
// 留 16px 内边距
const aw = vw - 16
const ah = vh - 16
return Math.min(aw / naturalWidth.value, ah / naturalHeight.value, 1)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

在计算适应窗口的缩放比例时,如果视口宽度或高度小于 16px(例如在窗口最小化或极端缩放调整的瞬间),awah 可能会计算为负数,从而导致 Math.min 返回负的缩放比例,使图片发生镜像翻转或渲染异常。建议使用 Math.max(1, ...) 确保可用宽高不为负数。

function computeFitScale(): number {
  const vp = viewportRef.value
  const vw = vp ? vp.clientWidth : window.innerWidth
  const vh = vp ? vp.clientHeight : window.innerHeight
  if (!naturalWidth.value || !naturalHeight.value || !vw || !vh) return 1
  // 留 16px 内边距,确保不为负数
  const aw = Math.max(1, vw - 16)
  const ah = Math.max(1, vh - 16)
  return Math.min(aw / naturalWidth.value, ah / naturalHeight.value, 1)
}

Comment on lines +209 to +210
// 闪烁标记:点击图上文字复制后,用 id 标记该行闪烁高亮(递增避免重复定时器冲突)。
const flashedIds = ref<number[]>([])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

flashedIds 声明为稀疏数组(number[])在 Vue 3 中可能会遇到响应性边缘问题(因为 Vue 的响应式数组主要追踪已有的索引和 length)。使用键值对对象(Record<number, number>)来管理行高亮状态更加安全、直观且性能更好。

// 闪烁标记:点击图上文字复制后,用 id 标记该行闪烁高亮(递增避免重复定时器冲突)。
const flashedIds = ref<Record<number, number>>({})

Comment on lines +256 to +264
/** 触发第 i 行的闪烁高亮(图上 + 列表),1.5s 后移除。 */
function flashLine(i: number): void {
activeIndex.value = i
const id = (flashedIds.value[i] ?? 0) + 1
flashedIds.value[i] = id
window.setTimeout(() => {
if (flashedIds.value[i] === id) flashedIds.value[i] = 0
}, 1500)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

配合将 flashedIds 重构为 Record<number, number>,更新 flashLine 函数。在定时器结束时,可以直接删除对应的键,保持响应式对象的干净整洁。

/** 触发第 i 行的闪烁高亮(图上 + 列表),1.5s 后移除。 */
function flashLine(i: number): void {
  activeIndex.value = i
  const id = (flashedIds.value[i] ?? 0) + 1
  flashedIds.value[i] = id
  window.setTimeout(() => {
    if (flashedIds.value[i] === id) {
      delete flashedIds.value[i]
    }
  }, 1500)
}

@lzx8589561 lzx8589561 merged commit 3929a40 into ZToolsCenter:main Jul 10, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants